Description
CDL provides a number of specific mechanisms for handling data in distributed parallel environments. Active objects (threads, call-backs, methods and circuits) can all own 'workspace' data objects which are also persistent, and in many ways provide an equivalent of thread specific data. Methods can also own 'state' objects, which are similar to workspace objects, but in the specific case of 'slave-able' methods, which can be scheduled to execute on any CPU, the state data object is automatically moved (and cached) to whichever machine executes the method instance. Because state data needs to be moved each time the target processor changes, it should only be used when necessary. Constants and tables that are calculated once and do not change (e.g. FFT twiddle coefficients) should always be stored in workspace.
In all cases, data objects have two components. The first of these is the 'data' component itself (see Data Components) which is referenced and managed by the second 'reference' component. The data component can contain native pointers and references but these will not be valid when data objects are moved between machines (only state data is moved), and are unlikely to be valid in asymmetric memory environments. They can be used however to reference objects within the data component but will need to be 're-linked' each time their owning store is opened for read.
State and Workspace Objects
The translator will create a skeletal class definition for each state and workspace data type, but will assume a fixed size data component (see Data Component section above).
State and Workspace reference object type names are derived from their data component type with "Obj" appended. In the case of state and workspace objects the reference component's type is automatically generated using the following schemes;
Method Workspace
MthdType +"MthdWorkspaceObj". If a method is explicitly defined then MthdType is specified by the definition. If the method is defined 'in-line' then it's type is constructed from parent circuit type, followed by "_" followed by its instance name. So if a method named "M" is defined in a circuit of type "MyCct" then its workspace data component's type would be 'MyCct_MMthdWorkspaceObj'.
Call-Back Workspace
CbfType +"CbfWorkspaceObj". If a call-back is explicitly defined then CbfType is specified by the definition. If the call-back is defined 'in-line' then it's type is constructed from parent circuit type, followed by "_" followed by its instance name. So if a call-back named "C" is defined in a circuit of type "MyCct" then its workspace data component's type would be 'MyCct_CCbfWorkspaceObj'.
Circuit Workspace
CctType +"CctWorkspaceObj". So if a circuit has type "MyCct" then its workspace data component's type would be 'MyCct_WorkspaceObj'.
Method State
MthdType +"MthdStateObj". If a method is explicitly defined then MthdType is specified by the definition. If the method is defined 'in-line' then it's type is constructed from parent circuit type, followed by "_" followed by its instance name. So if a method named "M" is defined in a circuit of type "MyCct" then its state data component's type would be 'MyCct_MMthdStateObj'.
The translator will produce the following workspace definition for a method of type 'Mtype';
class
MtypeMthdWorkspaceObj : public
ClpMthdWorkspaceObject<MtypeMthdWorkspace>
{
public:
/// <summary>
/// Workspace data class access method
/// </summary>
/// <returns>A reference to the workspace data class
for this workspace object</returns>
MtypeMthdWorkspace& Data()
{
return Ref();
}
/// <summary>
/// Determines workspace data class actual size
/// </summary>
/// <returns>The actual size of the workspace data
class for this workspace object</returns>
Uns Size()
{
if ( sizeof( MtypeMthdWorkspace ) ==
1 )
return 0;
else
return sizeof(
MtypeMthdWorkspace );
}
/// <summary>
/// Initializes the workspace data class
/// </summary>
/// <returns>TRUE if successful, FALSE if
failed</returns>
Uns Initialise()
{
return
Data().Initialise();
}
};
By default the translator will also produce the following initialization code for methods which will be called once during initialization. The other active objects will have similar code generated for them, but only methods support state.
The 'Definitive()' function will only return TRUE if the executing process owns the single definitive copy of the method's state. Unless the method is explicitly 'pinned' to execute in a particular slave process this will be the master. The 'Executable()' function will only return TRUE if the executing process is able to execute the method. If it is not slaved this will be the master, if it is pinned it will be a particular slave, otherwise it will return TRUE for all slaves. It should be noted therefore that when required, methods must be 'pinned' before state and workspace objects are constructed.
Uns MtypeMthdBaseElem::Initialise()
{
Uns failed = FALSE;
if ( Definitive() )
{
// Only methods can own
state
failed |= !
mState.Construct();
failed |= !
mState.Initialise();
}
if ( Executable() )
{
failed |= !
mWorkspace.Construct();
failed |= !
mWorkspace.Initialise();
}
return ! failed;
}
State and workspace data can then be accessed from user code using mState.Data() and mWorkspace.Data() respectively.
Variable Sized Data
As with records, if the data is fixed sized then the generated code above will suffice, but variable sized data components will require additional user code. As with records, the user needs to provide a 'SetDims', a 'Size', and a 'Construct'; all of which require data component sizes as parameters.
The examples that follow assume that the data object has the following form, where N and M are assumed to be dynamic (unspecified at compile time). See Data Components example.
class Mtype
{
Float f;
Uns N;
Uns M;
Int X[N];
Int Y[N][M];
};
If we assume that the data component follows the same convention as that outlined in the data components topic, then the method's workspace data component will be the following;
class MtypeMthdWorkspace : public
ClpNewObject
{
public:
Uns SetDims( Uns n, Uns m )
{
// Store current
dimensions
N = n;
M = m;
return TRUE;
}
// Calculate object size for n,m
Uns Size( Uns n, Uns m )
{
return sizeof(
MtypeMthdWorkspace ) + (m+1)*n*sizeof( Int );
}
// Calculate current object size
Uns Size()
{
return Size( N, M );
}
// Initialize object
Uns Initialise()
{
// Perform object specific
initialization here
return TRUE;
}
// Return current value of 'N'
Uns N_val()
{
return N;
}
// Return current value of 'M'
Uns M_val()
{
return M;
}
// Set current value of 'N'
void N_set( Uns n )
{
N = n;
}
// Set current value of 'M'
void M_set( Uns m )
{
M = m;
}
// Set X array element
void X_set( Uns idx, Int x )
{
//
Assume 'X' follows the 'MtypeMthdWorkspace' header contiguously
((Int*)(this+1))[ idx ] =
x;
}
// Set Y array element
void Y_val( Uns idx1, Uns idx2, Int
y )
{
// Assume Y follows X
contiguously
Int *Y_start =
((Int*)(this+1))+N;
Y_start[ idx1*M+idx2 ] =
y;
}
// Return X array element
Int X_val( Uns idx )
{
// Assume it follows the
'MtypeMthdWorkspace' header contiguously
return ((Int*)(this+1))[ idx
];
}
// Return Y array element
Int Y_val( Uns idx1, Uns idx2 )
{
// Assume Y follows X
contiguously
Int *Y_start =
((Int*)(this+1))+N;
return Y_start[ idx1*M+idx2
];
}
private:
// Fixed size components
Float f;
Uns N;
Uns M;
};
The generated reference object would then need to be updated as follows. The additional user code is highlighted in red.
class
MtypeMthdWorkspaceObj : public
ClpMthdWorkspaceObject<MtypeMthdWorkspace>
{
public:
/// <summary>
/// Workspace data class access method
/// </summary>
/// <returns>A reference to the workspace data class
for this workspace object</returns>
MtypeMthdWorkspace& Data()
{
return Ref();
}
/// <summary>
/// Determines workspace data class actual size
/// </summary>
/// <returns>The actual size of the workspace data
class for this workspace object</returns>
Uns Size()
{
return Data().Size();
}
/// <summary>
/// Initializes the workspace data class
/// </summary>
/// <returns>TRUE if successful, FALSE if
failed</returns>
Uns Initialise()
{
return
Data().Initialise();
}
// Two parameter SetDims
function (generic);
Uns SetDims( Uns n, Uns m )
{
return Data().SetDims( n, m
);
}
// Two parameter Size function (generic);
static Uns Size( Uns n, Uns m )
{
return
MtypeMthdWorkspace::Size( n, m );
}
// Two
parameter Construct function (generic);
Uns Construct( Uns n, Uns m )
{
Uns totalSize = Size( n, m
);
SetTsSize( totalSize );
if ( Construct() )
{
return
Data().SetDims( n, m );
}
return FALSE;
}
};
All three user modified functions above are generic and can therefore be used with any data component that has 2 size parameters and adopts the required convention. In fact the 3 or more size parameter cases are all identical but with the appropriate number of sizing parameters.
Now finally the method's initialise call needs to be re-coded so that it calls the user defined construct and initialise;
The generated harness's initialise will call the base initialise which is no longer required and should be replaced by the following generic code;
Uns MtypeMthdElem::Initialise()
{
Uns failed = FALSE;
// Retrieve dynamic sizes from file, command line, or similar;
Uns N_size = GetN_size();
Uns M_size = GetM_size();
if ( Definitive() )
{
failed |= ! mState.Construct( N_size, M_size
);
failed |= ! mState.Initialise();
}
if ( Executable() )
{
failed |= ! mWorkspace.Construct( N_size, M_size
);
failed |= ! mWorkspace.Initialise();
}
return ! failed;
}
The generic code above will also generalize to multidimensional dynamic data with 3 or more size parameters.